package agent;

import instanceXMLParser.CellLogicalState;

import java.awt.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.jgrapht.DirectedGraph;
import org.jgrapht.alg.DijkstraShortestPath;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;

import vca.Solution;

import aima.core.agent.Action;
import aima.core.agent.impl.DynamicAction;
import aima.core.agent.impl.NoOpAction;
import core.VacuumEnvironment.LocationState;

public class EnviromentMap {

	private Map<Point, LocationState> map;
	private Map<Point, Integer> dirtyAmounts;

	DirectedGraph<Point, DefaultEdge> relativeBoard;
	List<String> backPartialPath;

	private Point current;
	private Point base;
	private ArrayList<String> pathToTheBase;

	private Stack<Point> path;
	private int row;
	private int col;
	Point mins, maxs;

	boolean popped;
	boolean comingBackHome;
	boolean comingBackHomeNO_Dirty;

	private Point minRectCenter;

	public EnviromentMap(LocationState state, int row, int col) {
		popped = false;
		comingBackHomeNO_Dirty = false;
		comingBackHome = false;
		map = new HashMap<Point, LocationState>();
		dirtyAmounts = new HashMap<Point, Integer>();
		path = new Stack<Point>();
		pathToTheBase = new ArrayList<String>();
		backPartialPath = new ArrayList<String>();
		relativeBoard = new DefaultDirectedGraph<Point, DefaultEdge>(
				DefaultEdge.class);
		current = new Point(0, 0);
		map.put(current, state);
		updateGraph(current);

		// initializate minrect
		mins = new Point(0, 0);
		maxs = new Point(0, 0);
		this.row = row;
		this.col = col;

		minRectCenter = new Point();
		minRectCenter.x = Integer.MAX_VALUE;
		minRectCenter.y = Integer.MAX_VALUE;

	}

	public ArrayList<String> getPathToTheBase() {
		return pathToTheBase;
	}

	public void setPathToTheBase() {
		pathToTheBase = findPathBetweenTwoPoints(current, base);
	}

	public void updateGraph(Point newPoint) {
		relativeBoard.addVertex(new Point(newPoint));
		Point neighbor = new Point(newPoint);
		// up-------------------------------------------
		neighbor.x -= 1;// up neighbor
		if (relativeBoard.containsVertex(neighbor)) {
			relativeBoard.addEdge(new Point(newPoint), new Point(neighbor));
			relativeBoard.addEdge(new Point(neighbor), new Point(newPoint));
		}

		// down-------------------------------------------
		neighbor.x += 1;// original point
		neighbor.x += 1;// down point
		if (relativeBoard.containsVertex(neighbor)) {
			relativeBoard.addEdge(new Point(newPoint), new Point(neighbor));
			relativeBoard.addEdge(new Point(neighbor), new Point(newPoint));
		}

		// right-------------------------------------------
		neighbor.x -= 1;// original point
		neighbor.y += 1;// right point
		if (relativeBoard.containsVertex(neighbor)) {
			relativeBoard.addEdge(new Point(newPoint), new Point(neighbor));
			relativeBoard.addEdge(new Point(neighbor), new Point(newPoint));
		}

		// left-------------------------------------------
		neighbor.y -= 1;// original point
		neighbor.y -= 1;// left point
		if (relativeBoard.containsVertex(neighbor)) {
			relativeBoard.addEdge(new Point(newPoint), new Point(neighbor));
			relativeBoard.addEdge(new Point(neighbor), new Point(newPoint));
		}

		// System.err.println("aggiungo " + newPoint);

	}

	public static int RowMajIdx(int x, int y, int size) {
		return x * size + y;
	}

	private List<Integer> targetCell = new ArrayList<Integer>();
	private CellLogicalState[][] boardState;
	private Map<Integer, Integer> dirtyAmountsForSolver;

	private void createTargetCellAndBoardState() {
		int n = getDimMinRect().x;
		int m = getDimMinRect().y;
		// System.out.println(n + " " + m);
		boardState = new CellLogicalState[n][m];
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				boardState[i][j] = new CellLogicalState(1,
						LocationState.Obstacle);
			}
		}
		for (Point point : map.keySet()) {
			Point relativePoint = getMinRectRelativeCoords(point);
			if (map.get(point) == LocationState.Dirty) {
				targetCell.add(RowMajIdx(relativePoint.x, relativePoint.y,
						getDimMinRect().y));
			}
			if (!(relativePoint.x >= n || relativePoint.x < 0
					|| relativePoint.y >= m || relativePoint.y < 0))
				boardState[relativePoint.x][relativePoint.y].setLocState(map
						.get(point));
		}

		dirtyAmountsForSolver = new HashMap<Integer, Integer>();
		// creo costi suck per k solver
		for (Point p : dirtyAmounts.keySet()) {
			Point rl = getMinRectRelativeCoords(p);
			dirtyAmountsForSolver.put(RowMajIdx(rl.x, rl.y, m),
					dirtyAmounts.get(p));
		}

	}

	public AgentsManager prepareStuffForSolver(double energy,
			boolean startFromTheBase) {
		createTargetCellAndBoardState();
		AgentsManager solver;
		if (startFromTheBase)
			solver = new AgentsManager(8, getDimMinRect().x, getDimMinRect().y,
					targetCell, boardState, getMinRectRelativeCoords(current),
					getMinRectRelativeCoords(current), energy,
					dirtyAmountsForSolver);
		else
			solver = new AgentsManager(8, getDimMinRect().x, getDimMinRect().y,
					targetCell, boardState, getMinRectRelativeCoords(current),
					base, energy, dirtyAmountsForSolver);
		return solver;

	}

	public Stack<Point> getPath() {
		return path;
	}

	public void setPath(Stack<Point> path) {
		this.path = path;
	}

	public Map<Point, LocationState> getMap() {
		return map;
	}

	public void setMap(Map<Point, LocationState> map) {
		this.map = map;
	}

	public void updateMinRect(Point p) {
		if (p.x < mins.x) {
			mins.x = p.x;
		}
		if (p.x > maxs.x) {
			maxs.x = p.x;
		}

		if (p.y < mins.y) {
			mins.y = p.y;
		}
		if (p.y > maxs.y) {
			maxs.y = p.y;
		}

		double xCenter = (getDimMinRect().x) / 2;
		double yCenter = (getDimMinRect().y) / 2;

		double minDist = Double.MAX_VALUE;
		Point minPoint = new Point();

		Point tmp = null;
		for (Point c : map.keySet()) {
			if (map.get(c) != LocationState.Obstacle) {
				tmp = getMinRectRelativeCoords(c);
				if (Math.sqrt(Math.pow(xCenter - tmp.x, 2)
						+ Math.pow(yCenter - tmp.y, 2)) < minDist) {
					minDist = Math.sqrt(Math.pow(xCenter - tmp.x, 2)
							+ Math.pow(yCenter - tmp.y, 2));
					minPoint = tmp;
				}
			}
		}

		minRectCenter = minPoint;
	}

	public void addElementMap(String s, LocationState state, int dirtyAmount) {
		// par3 is null(or -1, or 0) if the cell is not of type dirty;

		if(s.equals("suck"))
			return;
		
		Point p = new Point();

		getPointFromDirection(s, p);

		
		map.put(p, state);
		if (state == LocationState.Dirty) {
			dirtyAmounts.put(p, dirtyAmount);
		}
//		System.err.println(p);
		// assumendo che se vai contro un ostacolo LocationState � uguale a
		// Obstacle
		if (state != LocationState.Obstacle) {

			if (!popped) {
				path.add(current);
				updateMinRect(p);
			}
			current = p;
			updateGraph(current);
		}
	}

	public void getPointFromDirection(String s, Point p) {
		if (s.equals("left")) {
			p.y = current.y - 1;
			p.x = current.x;
		} else if (s.equals("right")) {
			p.y = current.y + 1;
			p.x = current.x;
		} else if (s.equals("up")) {
			p.y = current.y;
			p.x = current.x - 1;
		} else if (s.equals("down")) {
			p.y = current.y;
			p.x = current.x + 1;
		}
	}

	public Point getCurrent() {
		return current;
	}

	public void setCurrent(Point current) {
		this.current = current;
	}

	public String getDirectionFromPoint(Point p, Point current) {
		if (current.y == p.y + 1) {
			return "left";
		} else if (current.y == p.y - 1) {
			return "right";
		} else if (current.x == p.x + 1) {
			return "up";
		} else if (current.x == p.x - 1) {
			return "down";
		}
		return null;
	}

	public boolean isExplored(String s) {
		Point p = new Point();

		getPointFromDirection(s, p);

		return map.containsKey(p);
	}

	public String getNextMove(Double energy) {

		probFindingHome(energy);

		if (backPartialPath.size() > 0)
			return backPartialPath.remove(0);

		ArrayList<String> unvisited = new ArrayList<String>();

		if (!map.containsKey(new Point(current.x + 1, current.y))) {
			unvisited.add("down");
		}
		if (!map.containsKey(new Point(current.x - 1, current.y))) {
			unvisited.add("up");
		}
		if (!map.containsKey(new Point(current.x, current.y + 1))) {
			unvisited.add("right");
		}
		if (!map.containsKey(new Point(current.x, current.y - 1))) {
			unvisited.add("left");
		}

		String tmp = null;
		if (unvisited.size() > 0) {
			int r = (int) (Math.random() * unvisited.size());
			tmp = unvisited.get(r);
			popped = false;
		} else {// ==0
			backPartialPath = getPathToTheFirstUnvisited();
			popped = true;
			if (backPartialPath.size() == 0)
				return "noop";
			tmp = backPartialPath.remove(0);
		}
		// System.err.println("mossa " + tmp + "  unvisited.size "
		// + unvisited.size());
		return tmp;

	}

	public double probFindingHome(Double energy) {
		int numExplored = map.keySet().size();
		// +2 perch� ci sono anche i muri esterni
		int tot = (row + 2) * (col + 2);
		int diff = tot - numExplored;
		double prop = energy / diff;

		return prop;

	}

	private ArrayList<String> getPathToTheFirstUnvisited() {

		updateGraph(current);
		// ce ne possono stare piu di non visitate. Riempi una lista di non
		// visitate e sceglile in modo randomzzzz
		boolean found = false;
		Point destinationPoint = null;
		while (!found) {
			if (path.size() == 0)
				return findPathBetweenTwoPoints(current, base);
			destinationPoint = path.pop();
			if (!map.containsKey(new Point(destinationPoint.x + 1,
					destinationPoint.y)))
				found = true;
			else if (!map.containsKey(new Point(destinationPoint.x - 1,
					destinationPoint.y)))
				found = true;
			else if (!map.containsKey(new Point(destinationPoint.x,
					destinationPoint.y + 1)))
				found = true;
			else if (!map.containsKey(new Point(destinationPoint.x,
					destinationPoint.y - 1)))
				found = true;
		}

		// System.out.println("current corrente è: " + current);

		return findPathBetweenTwoPoints(current, destinationPoint);
	}

	private ArrayList<String> findPathBetweenTwoPoints(Point startPoint,
			Point destinationPoint) {
		DijkstraShortestPath<Point, DefaultEdge> pathFinder = new DijkstraShortestPath<Point, DefaultEdge>(
				relativeBoard, startPoint, destinationPoint);

		List<DefaultEdge> tempStep;
		tempStep = pathFinder.getPathEdgeList();

		List<Point> completePath = new ArrayList<Point>();
		for (DefaultEdge defaultEdge : tempStep) {

			if (completePath.size() == 0
					|| !completePath.get(completePath.size() - 1).equals(
							(Point) relativeBoard.getEdgeSource(defaultEdge)))
				completePath.add((Point) relativeBoard
						.getEdgeSource(defaultEdge));

			completePath.add((Point) relativeBoard.getEdgeTarget(defaultEdge));

			// System.out.print((Integer)directedGraph.getEdgeSource(defaultEdge)+" "+(Integer)
			// directedGraph.getEdgeTarget(defaultEdge)+" " );
		}

		// System.err.println("il nodo goal è :" + destinationPoint);
		// System.err.println(completePath);

		return (ArrayList<String>) buildSolutionFromVertexPath(completePath);
	}

	private List<String> buildSolutionFromVertexPath(
			List<Point> completeSolutionPath) {
		List<String> solution = new ArrayList<String>();
		for (int i = 0; i < completeSolutionPath.size() - 1; i++) {
			Point from = completeSolutionPath.get(i);
			Point to = completeSolutionPath.get(i + 1);

			solution.add(getDirectionFromPoint(to, from));

		}// for

		// System.err.println("vediamo se funge " + solution);

		return solution;

	}

	public Point getMinRectRelativeCoords(Point p) {
		return new Point(p.x + Math.abs(mins.x), p.y + Math.abs(mins.y));
	}

	/**
	 * Aggiungiamo uno perchè con le coordinate ricaviamo l'ultime indicie
	 * valido ma a noi interessano le dimensioni
	 */
	public Point getDimMinRect() {
		// prendo dimensioni del rettangolo minimo
		Point dim = new Point();
		dim.x = maxs.x - mins.x + 1;
		dim.y = maxs.y - mins.y + 1;

		// trasformo le coordinate iniziali dell'agente nelle cordinate assolute
		// relative al rettangolo minimo

		return dim;

	}

	public Point getBase() {
		return base;
	}

	public void setBase(Point base) {
		this.base = base;
	}

	public Point getMinRectCentre() {
		return minRectCenter;
	}

	public void setMinRectCentre(Point minRectCentre) {
		this.minRectCenter = minRectCentre;
	}
}
